<?php
require_once 'SahORMException.php';
require_once 'SahORMSqlExpression.php';
require_once 'SahORMHandler.php';
class SahORM
{
  private $_class_name;
  private $_conn_name;
  private $_table_name;
  private $_from_static;
  private $_parents = array();
  private $_validate_error_msg;
  
  /**
   * Constructor del objeto
   * @param string $class_name Nombre de la clase
   * @param string $table_name Nombre de la tabla
   * @param string $conn_name Nombre de la conexion
   * @param bool $from_static TRUE si es llamado desde el metodo estatico
   *                          de lo contrario FALSE
   */
  public function __construct($class_name, $table_name, $conn_name,
    $from_static)
  {
    $this->_class_name = $class_name;
    $this->_conn_name = $conn_name;
    $this->_table_name = $table_name;
    $this->_from_static = $from_static;
   
    /*
     * Crear las propiedades del objeto referentes a los campos de la tabla
     * solo si el objeto no fue creado desde el metodo estatico
     */ 
    if($from_static == FALSE){
      foreach( $this->getColumnsInfo() as $ColInfo ){
        if(!isset($this->{$ColInfo->Field})){
          $this->{$ColInfo->Field} = NULL;
        }
      }
    }
  }
  
  /**
   * Metodo magico para responter a metodos como get, set, count, etc.
   */
  public function __call($method, $arguments)
  {
    /* 
     * # # # # # # # # # # Metodo GET # # # # # # # # # #
     */
    if(substr($method, 0, 3) == 'get'){
      /*
       * Obtener parent o childs
       */
      if(substr($method, -1, 1) == 's'){
        // Obtener hijos
        // Obtener el nombre de la clase ejemplo: "getChilds" = "Child"
        $child_class_name = substr(substr($method, 0, -1), 3);
        $ChildEmptyORM = new $child_class_name(TRUE);
        
        $criteria = array(
          'where' => $this->getTableName().'_id=:sahormparentid',
          'arguments' => array(':sahormparentid' => $this->id)
        );
        if(isset($arguments[0]) AND is_array($arguments[0])){
          $criteria = SahORMHandler::mergeCriteria($criteria, $arguments[0]);
        }
        
        return $ChildEmptyORM->find($criteria);
      }
      /* END: Get Childs */
      else {
        // Get parent
        // Obtener el nombre de la clase ejemplo: "getParent" = "Parent"
        $parent_class_name = substr($method, 3);
        if(!isset($this->_parents[$parent_class_name])){
          $ParentEmptyORM = new $parent_class_name(TRUE);
          $parent_id_field = $ParentEmptyORM->getTableName() . '_id';
          $this->_parents[$parent_class_name] = $ParentEmptyORM->findById(
            $this->$parent_id_field);
        }
        return $this->_parents[$parent_class_name];
      }
    }
    /* END: method get */
   
    /* 
     * # # # # # # # # # # Metodo SET # # # # # # # # # #
     */
    else if(substr($method, 0, 3) == 'set'){
      /*
       * Definir padre
       */
      // Obtener el nombre de la clase ejemplo: "setParent" = "Parent"
      $parent_class_name = substr($method, 3);
      if(isset($arguments[0])
        AND ($arguments[0] instanceof $parent_class_name)
        AND !$arguments[0]->isNew()){
          
        $this->{$arguments[0]->getTableName().'_id'} = $arguments[0]->id;
        
        // Return $this para linking
        return $this;
        
      } else {
        throw new SahORMException($this->getClassName() . '::' . $method .
        '() El argumento 1 debe ser un objeto de tipo ' . $parent_class_name .
        ' con un ID ya definido.', SahORMException::BAD_PARENT_ARGUMENT);
      }
    }
    /* END: method set */
    
    /* 
     * # # # # # # # # # # Metodo COUNT # # # # # # # # # #
     */
    elseif(substr($method, 0, 5) == 'count'){
      // Contar hijos
      // Obtener el nombre de la clase ejemplo: "countChilds" = "Child"
      $child_class_name = substr(substr($method, 0, -1), 5);
      $ChildEmptyORM = new $child_class_name(TRUE);
      
      $criteria = array(
        'where' => $this->getTableName().'_id=:sahormparentid',
        'arguments' => array(':sahormparentid' => $this->id)
      );
      
      // Crear criterio de COUNT
      if(isset($arguments[0]) AND is_array($arguments[0])){
        $criteria = SahORMHandler::mergeCriteria($criteria, $arguments[0]);
      }
      
      return $ChildEmptyORM->count($criteria);
    }
    /* END: method count */
    else {
      trigger_error('Llamada a metodo indefinido ' . $this->getClassName().
        '::' . $method . '()', E_USER_ERROR);
    }
  }

  /**
   * Realiza un SELECT COUNT(*) sobre la tabla del ORM con los criterios
   * especificados
   * @param array $criteria Criterios para el SELECT COUNT(*)
   * @return int Total del conteo
   */
  public function count($criteria = array())
  {
    return SahORMHandler::count($criteria, $this->_table_name,
      $this->_conn_name);
  }

  /**
   * Guarda los datos del ORM en su tabla especifica utilizando su conexion
   * Devuelve TRUE en caso de exito, de lo contrario FALSE ( si !validate() )
   * @return bool
   */
  public function save()
  {
    if(!$this->validate()){
      return FALSE;
    } else {
    	
      // Crear nombres de campos a guardar
      $fields = array();
      foreach($this->getColumnsInfo() as $ColInfo){
        $fields[$ColInfo->Field] = $this->{$ColInfo->Field};
      }
      
      if($this->isNew()){
        /*
         * INSERT
         */
        $St = SahORMHandler::insert($this->_table_name, $fields,
          $this->_conn_name);
        
        // Actualizar los datos
        $this->id = SahORMHandler::getPDO($this->_conn_name)->lastInsertId();
      } else {
        /*
         * UPDATE
         */
        $St = SahORMHandler::update($this->_table_name, $fields, array(
          'where' => 'id=:id',
          'arguments' => array(':id' => $this->id)
        ), $this->_conn_name);
      }
      
      $this->refresh();
      return TRUE;
    }
  }
  
  /**
   * Busca un grupo de objetos y devuelve un array
   * @param array $criteria Criterio de busqueda
   * @param bool $return_first Si es TRUE, solo devuelve el primer objeto
   * @return array(SahORM)|SahORM|FALSE
   */
  public function find($criteria = array(), $return_first = FALSE)
  {
    $St = SahORMHandler::select('*', $this->_table_name, $criteria);
    $St->setFetchMode(PDO::FETCH_CLASS, $this->_class_name);
    if($return_first){
      return $St->fetch();
    } else {
      return $St->fetchAll();
    }
  }
  
  /**
   * Mismo comportamiento que find() pero solo devuelve un objeto ORM o FALSE
   * si no se encuentra el registro con los criterios $criteria
   * @param array $criteria Criterios de seleccion
   * @return SahORM
   */
  public function findOne($criteria = array())
  {
    return $this->find($criteria, TRUE);
  }
  
  /**
   * Busca un registro por ID y devuelve su ORM
   * @param int $id ID del registro
   * @return SahORM |FALSE
   */
  public function findById($id)
  {
    return $this->find(array(
      'where' => 'id=:id',
      'arguments' => array(':id' => $id)
    ), TRUE);
  }
  
  /**
   * Realiza un UPDATE de los campos $fields sobre la tabla del objeto con
   * el criterio $criteria
   * @param array Matriz key-value de campos
   * @param array Criterio de UPDATE
   * @return int Numero de filas actualizadas
   */
  public function update($fields, $criteria = null)
  {
    if($criteria == null){
      $criteria = array('where' => 'id=:id', 'arguments' => array(':id'=>$this->id));
    }
    
    $St = SahORMHandler::update($this->_table_name, $fields, $criteria,
    $this->_conn_name);
    return $St->rowCount();
  }
 
  /**
   * Realiza un DELETE con los criterios $criteria sobre la tabla del objeto
   * Si $criteria no es definido, se borra el registro del ORM actual.
   * @param array $criteria Criterios del DELETE
   * @return int Numero de filas eliminadas
   */
  public function delete($criteria = NULL)
  {
    if($criteria === NULL AND !$this->_from_static AND !$this->isNew()){
      // Criterio para eliminar al ORM actual
      $criteria = array('where' => 'id=:id', 'arguments' => array(
        ':id' => $this->id
      ));
    }
    $St = SahORMHandler::delete($this->_table_name, $criteria,
      $this->_conn_name);
    return $St->rowCount();
  }
  
  /**
   * Actualiza los datos del objeto
   */
  public function refresh()
  {
    if(!$this->isNew()){
      $St = SahORMHandler::select('*', $this->_table_name, array(
        'where' => 'id=:id',
        'arguments' => array(':id' => $this->id)
      ));
      $St->setFetchMode(PDO::FETCH_INTO, $this);
      $St->fetch();
    }
  }
 
  /**
   * Devuelve TRUE si el objeto es nuevo (no guardado) de lo contrario
   * devuelve FALSE
   * @return bool
   */
  public function isNew()
  {
    return ($this->id === NULL);
  }
 
  /**
   * Valida y parsea los valores de las columnas antes de guardar los datos
   * del ORM
   * @return bool Si es TRUE guarda, de lo contrario aborta
   */
  protected function validate()
  {
    return TRUE;
  }
  
  /**
   * Establece un mensaje de error, este metodo debe ser invocado desde
   * el metodo validate(), el mensaje del error puede ser recuperado con
   * getValidateError()
   * @param string $error_msg Mensaje del error
   */
  protected function setValidateError($error_msg)
  {
    $this->_validate_error_msg = $error_msg;
  }
  
  /**
   * Devuelve el error establecido por setValidateError()
   * @return string
   */
  public function getValidateError()
  {
    return $this->_validate_error_msg;
  }
  
  /**
   * Devuelve la informacion de las columnas de la tabla relacionada al objeto
   * @return array(object)
   */
  public function getColumnsInfo()
  {
    return SahORMHandler::getColumnsInfo($this->_table_name,
      $this->_conn_name);
  }
  
  /**
   * Devuelve el nombre de la clase
   * @return string
   */
  public function getClassName()
  {
    return $this->_class_name;
  }
  
  /**
   * Devuelve el nombre de la tabla relacionada al objeto
   * @return string
   */
  public function getTableName()
  {
    return $this->_table_name;
  }
  
  /**
   * Devuelve el nombre de la conexion relacionada al objeto
   * @return string
   */
  public function getConnName()
  {
    return $this->_conn_name;
  }
}
